因为2020.4起,所有app必须使用xcode11打包,所以近期处理了一下iOS13的适配工作。这里做一下记录,列举一下遇到的问题及解决办法。
1、 UIStatusBarStyle
UIStatusBarStyle 为 UIStatusBarStyleDefault(iOS13以下,黑字。iOS13以上,随系统深浅色模式,浅色模式黑字,深色模式白字),不受info.pilst文件中禁用暗黑模式的key的影响。使用hook处理。
@implementation UIApplication (Fix_13)
+ (void)initialize
{
if (@available(iOS 13.0, *)) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleInstanceSelector:@selector(setStatusBarStyle:) withNewSelector:@selector(MDFix_setStatusBarStyle:)];
[self swizzleInstanceSelector:@selector(setStatusBarStyle:animated:) withNewSelector:@selector(MDFix_setStatusBarStyle:animated:)];
});
}
}
- (void)MDFix_setStatusBarStyle:(UIStatusBarStyle)statusBarStyle {
if (statusBarStyle == UIStatusBarStyleDefault) {
if (@available(iOS 13.0, *)) {
[self MDFix_setStatusBarStyle:UIStatusBarStyleDarkContent];
return;
}
}
[self MDFix_setStatusBarStyle:statusBarStyle];
}
- (void)MDFix_setStatusBarStyle:(UIStatusBarStyle)statusBarStyle animated:(BOOL)animated {
if (statusBarStyle == UIStatusBarStyleDefault) {
if (@available(iOS 13.0, *)) {
[self MDFix_setStatusBarStyle:UIStatusBarStyleDarkContent animated:animated];
return;
}
}
[self MDFix_setStatusBarStyle:statusBarStyle animated:animated];
}
@end
2、 UIActivityIndicatorView
UIActivityIndicatorView 颜色和样式修改
typedef NS_ENUM(NSInteger, UIActivityIndicatorViewStyle) {
UIActivityIndicatorViewStyleMedium API_AVAILABLE(ios(13.0), tvos(13.0)) = 100,
UIActivityIndicatorViewStyleLarge API_AVAILABLE(ios(13.0), tvos(13.0)) = 101,
UIActivityIndicatorViewStyleWhiteLarge API_DEPRECATED_WITH_REPLACEMENT("UIActivityIndicatorViewStyleLarge", ios(2.0, 13.0), tvos(9.0, 13.0)) = 0,
UIActivityIndicatorViewStyleWhite API_DEPRECATED_WITH_REPLACEMENT("UIActivityIndicatorViewStyleMedium", ios(2.0, 13.0), tvos(9.0, 13.0)) = 1,
UIActivityIndicatorViewStyleGray API_DEPRECATED_WITH_REPLACEMENT("UIActivityIndicatorViewStyleMedium", ios(2.0, 13.0)) API_UNAVAILABLE(tvos) = 2,
};
// iOS13,UIActivityIndicatorViewStyle只有 Medium 和 Large 两种,用来控制view大小。默认为 Medium,颜色使用属性直接自定义。
- (UIActivityIndicatorView *)indicatorView
{
if (!_indicatorView) {
_indicatorView = [[UIActivityIndicatorView alloc] init];
_indicatorView.frame = CGRectMake(0, 0, 45, 45);
if (@available(iOS 13.0, *)) {
_indicatorView.activityIndicatorViewStyle = UIActivityIndicatorViewStyleLarge;
_indicatorView.color = [UIColor redColor]; //颜色可以自己随便定义
}
}
return _indicatorView;
}
3、UIModalPresentationStyle
UIViewController UIModalPresentationStyle 默认值变化。iOS13上,默认为UIModalPresentationAutomatic,iOS13以下,默认为UIModalPresentationFullScreen。
// UIViewController
/* If this property has been set to UIModalPresentationAutomatic, reading it will always return a concrete presentation style.
By default UIViewController resolves UIModalPresentationAutomatic to UIModalPresentationPageSheet,
but system-provided subclasses may resolve UIModalPresentationAutomatic to other concrete presentation styles.
articipation in the resolution of UIModalPresentationAutomatic is reserved for system-provided view controllers.
*/
@property(nonatomic,assign) UIModalPresentationStyle modalPresentationStyle API_AVAILABLE(ios(3.2));
/*
这个属性有个坑,即你set的值和get的值,可能不一样。
如果这个属性被设置为UIModalPresentationAutomatic,
当使用get方法读取这个属性时,会被系统自动解析为具体的 presentation style,默认是解析为UIModalPresentationPageSheet。
但是系统提供的其他子类,也可能解析成别的style。
*/
如上所示,所以想通过hook的方法来统一处理,就面临一个麻烦,没法锚定UIModalPresentationAutomatic这个值。
所以这里采用一个 bool 值记录是否手动set过presentation style。代码如下:
@interface UIViewController ()
@property (nonatomic, assign) BOOL hasSetPresentStyle;
@end
@implementation UIViewController (FixPresent)
+ (void)initialize
{
if (@available(iOS 13.0, *)) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleInstanceSelector:@selector(setModalPresentationStyle:) withNewSelector:@selector(MDFix_setModalPresentationStyle:)];
[self swizzleInstanceSelector:@selector(modalPresentationStyle) withNewSelector:@selector(MDFix_modalPresentationStyle)];
});
}
}
- (BOOL)hasSetPresentStyle {
NSNumber *hasSetPresentStyle = objc_getAssociatedObject(self, _cmd);
return [hasSetPresentStyle boolValue];
}
- (void)setHasSetPresentStyle:(BOOL)hasSetPresentStyle {
objc_setAssociatedObject(self, @selector(hasSetPresentStyle), @(hasSetPresentStyle), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)MDFix_setModalPresentationStyle:(UIModalPresentationStyle)modalPresentationStyle {
self.hasSetPresentStyle = YES;
[self MDFix_setModalPresentationStyle:modalPresentationStyle];
}
- (UIModalPresentationStyle)MDFix_modalPresentationStyle {
if (@available(iOS 13.0, *)) {
UIModalPresentationStyle style = [self MDFix_modalPresentationStyle];
//如果读取到的style是UIModalPresentationPageSheet,且没有手动设置过style。
if (style == UIModalPresentationPageSheet && !self.hasSetPresentStyle) {
//过滤系统的controller,即过滤 'UI' 开头的和 '_' 开头的。
NSString *className = NSStringFromClass([self class]);
if ([self isKindOfClass:[UINavigationController class]]) {
className = NSStringFromClass([((UINavigationController *)self).topViewController class]);
}
if (![className hasPrefix:@"UI"] && ![className hasPrefix:@"_"]) {
return UIModalPresentationFullScreen;
}
}
}
return [self MDFix_modalPresentationStyle];
}
@end
4、 UISearchBar 和 UISearchDisplayController
- UISearchDisplayController 替换为 UISearchController ,iOS13 彻底弃用 UISearchDisplayController,继续使用会直接crash。
- UISearchBar 定制UI,因为iOS13 禁止通过 [valueForKey:]方法获取私有属性,禁止remove UISearchBarBackground 这个view。
因为这块整体变动较大,且网上很多解决办法,所以这里不细说,具体方法参考google。
5、 window视图层级变化
window基础视图层级变化,第一个子视图不在是rootController.view(UILayoutContainerView)。变为UITransitionView。
window层级变化UIView *frontView = [[window subviews] firstObject];
id nextResponder = [frontView nextResponder];
//iOS13以下,返回当前window显示的controller
//iOS13,返回nil。因为frontView是UITransitionView,这个view是系统用来做动画的,这个view的nextResponder为nil。
//所以这里也衍生了一个问题,在iOS13上,如下代码会在push或pop时产生问题。
self.window.backgroundColor = [UIColor clearColor];
[self.window addSubview:rootNavController.view];
self.window.rootViewController = rootNavController;
//因为window不能直接addSubview:rootNavController.view。直接使用
//self.window.rootViewController = rootNavController; 即可
6、 UIApplication 获取 statusBar 或 statusBarWindow
UIApplication 获取 statusBar 或 statusBarWindow,都会crash。如下代码:
UIView *statusBar = [[[UIApplication sharedApplication] valueForKey:@"statusBarWindow"] valueForKey:@"statusBar"];
iOS13上,如果想要获取statusBar的 hidden、frame。推荐使用 UIStatusBarManager。
//UIStatusBarManager 声明如下:
@interface UIStatusBarManager : NSObject
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
@property (nonatomic, readonly) UIStatusBarStyle statusBarStyle;
@property (nonatomic, readonly, getter=isStatusBarHidden) BOOL statusBarHidden;
@property (nonatomic, readonly) CGRect statusBarFrame; // returns CGRectZero if the status bar is hidden
@end
UIStatusBarManager *smanager = [[[[UIApplication sharedApplication].delegate window] windowScene] statusBarManager];
7、 statusBar 和 navigationBar 重叠
iOS13,在非刘海屏的情况下,如果上一个 controller 隐藏了statusBar,在 viewWillDisappear 的时候显示。当push到下一个 controller 的时候,导航栏和状态栏会重叠。这应该是iOS13的一个bug,暂时通过如下代码进行修复:
@implementation UIViewController (FixStatusBar)
+ (void)initialize
{
if (@available(iOS 13.0, *)) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleInstanceSelector:@selector(viewDidAppear:) withNewSelector:@selector(MDFix_navigationBarFrame_viewDidAppear:)];
});
}
}
- (void)MDFix_navigationBarFrame_viewDidAppear:(BOOL)animated {
[self MDFix_navigationBarFrame_viewDidAppear:animated];
[self.navigationController.view setNeedsLayout];
}
@end
在 controller viewDidAppear 的时候,强行让导航控制器重新layout一次。这种改法,会导致push动画的时候,导航栏和状态栏依然重叠,动画结束后强制刷新,然后变正常。所以导致的结果就是,屏幕在push完成后闪了一下,效果并不是特别好。这里暂时没有特别好的解决方案。
8、 h5调起打电话
在 iOS13.0、iOS13.1、iOS13.3.1 上,使用 UIWebView 显示页面,如果 h5 使用<a herf="tel:010-123456789">
这种方式调起打电话,结果出出现奇怪的bug,会调起发短信的弹框。 而在 iOS13.2、iOS13.3 上,又是正常的。目前只能认为是系统的一个bug。(不知道是不是和苹果不打算在支持UIWebView有关系)
解决办法:拦截 webview 请求,判断 scheme, 具体代码如下:
- (BOOL)webView:(UIWebView *)aWebView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
...
NSURL *url = request.URL;
NSString *scheme = [url scheme];
...
if ([scheme isEqualToString:@"tel"]) {
if (@available(iOS 13.0, *)) {
NSString *telNumber = [url resourceSpecifier];
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"telprompt://%@", telNumber]] options:@{} completionHandler:nil];
return NO;
}
}
...
return YES;
}
9、 [NSData description]
工程中有个 API 接口,把图片转成 NSData,然后通过 AFNetwork 发送 post 请求,参数放在 httpBody 中,使用key1=value1&key2=value2
这种方式,将图片和参数一起上传。在iOS13以下,一直是正常的。但是在iOS13,图片上传一直失败。
具体原因如下:
NSData *data = ...
....
NSString *description1 = [NSString stringWithFormat:@"%@", data];
NSString *description2 = [data description];
上面的代码,在 iOS13 之前,description1
和 description2
都如下:
@"<44154da7 32345001 53106883 ffc1071f a70871e5 32345001 a59c0d24 a70871e5 aa8dbb41>"
在iOS13上,则表现如下:
@"{length =36, bytes = 0x44154da7 32345001 53106883 ffc1071f ... a70871e5 aa8dbb41}"
不再是 Data 本身的全部字节码,而是 length 加上 缩写的字节码。 导致 AFNetwork 在拼键值对字符串的时候,NSData 的内容丢失。所以图片上传不成功。
修改办法有以下3种:
- 1、手动将 NSData 转成字节码字符串。
- (NSString *)hexadecimalString: (NSData *)data {
const unsigned char *dataBuffer = (const unsigned char *)[data bytes];
if (!dataBuffer) {
return @"";
}
NSUInteger dataLength = [data length];
NSMutableString *hexString = [NSMutableString stringWithCapacity:(dataLength * 2)];
for (int i = 0; i < dataLength; ++i) {
[hexString appendFormat:@"%02x", (unsigned int)dataBuffer[i]];
}
return [NSString stringWithString:hexString];
}
- 2、将 NSData 进行 Base64 编码,服务端进行 Base64 解码。
- 3、使用
[NSData debugDescription]
方法。
优缺点:
第一种方法,比较稳定,但是如果 NSData 数据量大,可能会比较耗费性能。
第二种方法,效率比第一种稍快,但是需要服务端一起改动,工作量稍大
第三种方法,修改最简单,但是最不稳定。苹果宣布不应该使用description debugDescription
方法,因为他们随时有可能会改变输出样式。
最后,为了统一修复这个问题,对 [NSData description]
方法进行hook。
// NSData+Description.h
@interface NSData (Description)
+ (void)startHook;
@end
// NSData+Description.m
@implementation NSData (Description)
+ (void)startHook
{
if (@available(iOS 13.0, *)) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleInstanceSelector:@selector(description) withNewSelector:@selector(MDFix_description)];
});
}
}
- (NSString *)MDFix_description {
// 因为这种改法并不稳定,所以预计在这里加上打点上报,看下有哪里会调到这里,然后对应修改。这里只作为最后的兜底策略。
return [self debugDescription];
}
@end
10、 iOS 系统默认字体不正确
iOS 13 中,使用 CTFontCreateWithName(fontName, fontSize, NULL)
创建字体时,如果 fontName
传入的是系统默认字体,即 .SFUI-Regular
。控制台会输入如下信息:
CoreText performance note: Client called CTFontCreateWithName() using name ".SFUI-Regular" and got font with PostScript name "TimesNewRomanPSMT". For best performance, only use PostScript names when calling this API.
也就是说,系统会返给你 TimesNewRomanPSMT
字体,而不是系统的默认字体。解决办法是不使用 CTFontCreateWithName()
方法。
UIFont *font = [UIFont systemFontOfSize:15];
CTFontRef fontRef = CTFontCreateWithName((__bridge CFStringRef)font.fontName, font.pointSize, NULL);
...
CFRelease(fontRef);
// 上面的代码,在iOS13上,会出现字体变成罗马字体的bug,应该修改为:
UIFont *font = [UIFont systemFontOfSize:15];
CTFontRef fontRef = (__bridge CTFontRef)font;
// 这里注意,因为这里是直接转为 `CTFontRef` ,所以后面不需要 `CFRelease(fontRef)`
网友评论