iOS 13 适配

iOS 13 如期而至,适配工作可以开展起来啦。在适配 iOS 13 过程中,遇到了如下一些问题。


1. UITextField 的私有属性 _placeholderLabel 被禁止访问了

遇到的第一个崩溃是修改UITextFieldplaceholder的颜色,历史遗留代码如下:

[_textField setValue:self.placeholderColor forKeyPath:@"_placeholderLabel.textColor"];

收到的错误信息⚠️

'Access to UITextField's _placeholderLabel ivar is prohibited. This is an application bug' 

那么这个问题如何处理呢?

其实,UITextField有个attributedPlaceholder的属性,我们可以自定义这个富文本来达到我们需要的结果。

修改如下:

NSMutableAttributedString *placeholderString = [[NSMutableAttributedString alloc] initWithString:placeholder attributes:@{NSForegroundColorAttributeName : self.placeholderColor}];
_textField.attributedPlaceholder = placeholderString;

注意⚠️,iOS 13 通过 KVC 方式修改私有属性,有 Crush 风险,谨慎使用!


2. 控制器的 modalPresentationStyle 默认值变了

对于这个变化,有点措手不及,直接修改了模态窗口的交互。
查阅了下 UIModalPresentationStyle枚举定义,赫然发现iOS 13新加了一个枚举值:

typedef NS_ENUM(NSInteger, UIModalPresentationStyle) {
    UIModalPresentationFullScreen = 0,
    UIModalPresentationPageSheet API_AVAILABLE(ios(3.2)) API_UNAVAILABLE(tvos),
    UIModalPresentationFormSheet API_AVAILABLE(ios(3.2)) API_UNAVAILABLE(tvos),
    UIModalPresentationCurrentContext API_AVAILABLE(ios(3.2)),
    UIModalPresentationCustom API_AVAILABLE(ios(7.0)),
    UIModalPresentationOverFullScreen API_AVAILABLE(ios(8.0)),
    UIModalPresentationOverCurrentContext API_AVAILABLE(ios(8.0)),
    UIModalPresentationPopover API_AVAILABLE(ios(8.0)) API_UNAVAILABLE(tvos),
    UIModalPresentationBlurOverFullScreen API_AVAILABLE(tvos(11.0)) API_UNAVAILABLE(ios) API_UNAVAILABLE(watchos),
    UIModalPresentationNone API_AVAILABLE(ios(7.0)) = -1,
    UIModalPresentationAutomatic API_AVAILABLE(ios(13.0)) = -2,
};

是的,就是UIModalPresentationAutomatic,苹果居然直接将modalPresentationStyle默认值改成这个,有点不解,难道是怕我们不知道新加了这个交互?这个也完全违反了开闭原则吧😒。

如何修改:
如果你完全接受苹果的这个默认效果,那就不需要去修改任何代码。
如果,你原来就比较细心,已经设置了modalPresentationStyle的值,那你也不会有这个影响。
对于想要找回原来默认交互的同学,直接设置如下即可:

self.modalPresentationStyle = UIModalPresentationFullScreen;

值得注意的是,当 modalPresentationStyleUIModalPresentationAutomatic时,presentationController 是不会消失的。所以,关闭模态窗口的时候,presentationController 的生命周期方法 viewWillAppear:viewDidAppear: 都不会触发。


3. MPMoviePlayerController 在iOS 13已经不能用了

在使用到MPMoviePlayerController的地方,直接抛了异常:

'MPMoviePlayerController is no longer available. Use AVPlayerViewController in AVKit.' 

如何修改:
这个没啥好说的,既然不能再用了,那只能换掉了。替代方案就是AVKit里面的那套播放器。


4. iOS 13 DeviceToken有变化‼️

这个很重要⚠️
可能大多数使用第三方推送的童鞋都不会注意到这个问题,一般现在的第三方推送都是将DeviceToken原始数据丢进去,具体的解析都是第三方内部处理,所以,这些第三方解析DeviceToken的方式正确的话,那就毫无问题。如果你们是通过这种方式来获取DeviceToken,那你需要注意了。(这个坑也是多年前埋下的,很多文章介绍的也是下面这个方法,不规范的做法迟早要还的🤣),如下:

NSString *dt = [deviceToken description];
dt = [dt stringByReplacingOccurrencesOfString: @"<" withString: @""];
dt = [dt stringByReplacingOccurrencesOfString: @">" withString: @""];
dt = [dt stringByReplacingOccurrencesOfString: @" " withString: @""];

这段代码运行在 iOS 13 上已经无法获取到准确的DeviceToken字符串了,iOS 13 通过[deviceToken description]获取到的内容已经变了。

{length = 32, bytes = 0x778a7995 29f32fb6 74ba8167 b6bddb4e ... b4d6b95f 65ac4587 }

可以看到,跟原来我们认识的那个已经完全不一样了。其实,造成这样的问题,主要还是没有使用正确的方式来操作,下面是解决办法:

NSMutableString *deviceTokenString = [NSMutableString string];
const char *bytes = deviceToken.bytes;
NSInteger count = deviceToken.length;
for (int i = 0; i < count; i++) {
    [deviceTokenString appendFormat:@"%02x", bytes[i]&0x000000FF];
}

或者你也可以使用友盟提供的方法(2019年7月24日更新)

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
    if (![deviceToken isKindOfClass:[NSData class]]) return;
    const unsigned *tokenBytes = [deviceToken bytes];
    NSString *hexToken = [NSString stringWithFormat:@"%08x%08x%08x%08x%08x%08x%08x%08x",
                          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);
}

5.Sign in with Apple (提供第三方登录的注意啦⚠️)

如果你的应用使用了第三方登录,那么你可能也需要加下 「Sign in with Apple」🤪

Sign In with Apple will be available for beta testing this summer. It will be required as an option for users in apps that support third-party sign-in when it is commercially available later this year.

关于如何集成,可以参考这篇文章:《Sign in with Apple》
附上官方Demo:点我下载

Sign in with Apple 适配时间苹果已经确认,具体更新到了公众号同名文章,可以在文末扫码关注公众号进行查看。


6.即将废弃的 LaunchImage

从 iOS 8 的时候,苹果就引入了 LaunchScreen,我们可以设置 LaunchScreen来作为启动页。当然,现在你还可以使用LaunchImage来设置启动图。不过使用LaunchImage的话,要求我们必须提供各种屏幕尺寸的启动图,来适配各种设备,随着苹果设备尺寸越来越多,这种方式显然不够 Flexible。而使用 LaunchScreen的话,情况会变的很简单, LaunchScreen是支持AutoLayout+SizeClass的,所以适配各种屏幕都不在话下。

注意⚠️: 从2020年4月开始,所有使⽤ iOS13 SDKApp 将必须提供 LaunchScreenLaunchImage即将退出历史舞台。

再补充一点,在使用 LaunchScreen的时候,里面用到的图片资源,最好别放在 xcassets 里面,不然在你修改图片后,你会发现真机上并不会生效。


7. Dark Mode

Apps on iOS 13 are expected to support dark mode
Use system colors and materials
Create your own dynamic colors and images Leverage flexible infrastructure


9月24更新:

关于Dark Mode,这里补充几点。

因为苹果目前还没有强制必须适配这个,相信大家的很多项目也没有开始是配这个模式。所以,前期可以强制Light模式。不然,你可能会遇到一些问题,比如UITableViewCell的背景色,如果你没有设置过背景色的话,它在Dark模式下就是黑色的,再比如UIDatePicker文字颜色等等。

那么怎么强制模式呢?

iOS 13UIViewUIViewController都添加了一个属性:

@property (nonatomic) UIUserInterfaceStyle overrideUserInterfaceStyle API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);

给这个属性设置成某一种模式,即可强制显示模式。
如果你想修改一处,应用所有地方,那么你只需要设置widow的显示模式即可,这会影响widow下面的所有视图显示模式,这也可以看出显示模式是向下传递的。

if (@available(iOS 13, *)) {
   [self.window setOverrideUserInterfaceStyle:UIUserInterfaceStyleLight];
}

8.iOS 13 UITabBar顶部分割线隐藏

这个问题源自网友的提问,我的项目中并没有这样的需求,所以之前没有处理。
看到网上的一些解决办法,如下:

[UITabBar appearance].layer.borderWidth = 0.0f;
[UITabBar appearance].clipsToBounds = YES;

看了下,实际起作用的代码是这条代码:

[UITabBar appearance].clipsToBounds = YES;

也就是裁剪掉了多余的部分(多余的部分正好就是分割线,后面会提到为什么是分割线),达到隐藏分割线的目的。但是这样设置,TabBar会裁剪子视图,这样我们有大按钮的TabBar,按钮就会被裁剪,造成显示不完整。

那么在iOS 13上有没有别的办法来隐藏分割线呢?

肯定是有的,其实解决这样的问题,只要我们能找到这个视图,就可以解决问题。从这个角度出发,我们来看下iOS 13上面TabBar子视图都有哪些。直接打印下subviews即可。

(
    "<_UIBarBackground: 0x7fbb34007920; frame = (0 0; 414 83);",
    "<UITabBarButton: 0x7fbb2ed032d0; frame = (2 1; 410 48);"
)

我们可以看到内部有个_UIBarBackground私有的东西,分割线肯定跟他有关(总不可能跟UITabBarButton有关吧😝)。然后,我们在看下这个控件的子视图。

(
    "<UIImageView: 0x7fbb2ec09b90; frame = (0 -0.333333; 414 0.333333);",
    "<UIVisualEffectView: 0x7fbb2ec05a20; frame = (0 0; 414 83); "
)

emmmm🤔,我们看到了一个越界的UIImageView,这个UIImageView就是分割线视图。怎么证明呢?我们加下这个代码:

[self.tabBar.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    [obj.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj1, NSUInteger idx, BOOL * _Nonnull stop) {
         if ([obj1 isKindOfClass:[UIImageView class]]) {
             obj1.hidden = YES;
         }
     }];
 }];

运行后,发现分割线隐藏了,也就证明了我们的猜想。当然,上面这个代码不严谨哈,这里主要是提供一种思路,仅作为演示,具体怎么做相信大家都可以的。

总结下怎么解决:找到它》隐藏它。千万别通过KVC的方式去处理,理由看第一点。

9.UIWebView被废弃

"No longer supported; please adopt WKWebView.", ios(2.0, 12.0)

这个大家也尽快适配吧,看到有提到审核被拒的问题。

10.iOS 13 需增加蓝牙权限描述

简友 @funkep 提到最新一次提审被拒情况,被拒原因之一就是需要添加蓝牙权限获取描述。如果你的应用需要使用蓝牙权限,需要在Info.plist里面加上NSBluetoothAlwaysUsageDescription这个key,对应的描述value根据权限的用途来描述即可。

感谢 @funkep 的分享,大家遇到的被拒理由可以反馈到留言区,我会更新到文章里面,让更多人看到。


⚠️重要 本文不再维护

为什么呢?因为很多人抄袭,而且抄袭的人不在少数,我在简书举报过的不少于5个,完全照抄。我说的不是转载,转载好歹有个原文链接。更好笑的是在微信群,正好我也在这个群,一位抄袭者发了我的文章,还标了原创,不巧正好被我看到了。呵呵,圈子真小啊🙄。抄袭者都这么猖狂了,拿着别人的成果给自己的公众号拉流量。呵呵~

简书上的这位用户 @诸葛小亮 ,让我震惊!第一次让我感受到抄袭有理!不仅对抄袭和侵权没有概念,还骂我不要脸,质问我抄袭谁的。心虚,删我评论!

针对这些情况,我是失望透顶的!本来我觉得分享事件很好的事,直到我的三观被慢慢颠覆,我不想再去维权!所以到此为止!

对于简书我也是失望透顶,这里存在各种抄袭。

大家可以关注我的公众号,以后所有文章只会发到公众账号。


如果这篇文章对你有帮助,不妨随手点个赞!谢谢❤️

⚠️禁止未授权转载,只接收链接转载,不接受内容拷贝转载