美文网首页
iOS13 适配

iOS13 适配

作者: whlpkk | 来源:发表于2020-02-14 12:45 被阅读0次

因为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 之前,description1description2 都如下:

@"<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)`

相关文章

  • iOS13 适配问题 看这一篇就够了

    技术参考: apple login IOS13适配-详细 iOS 13 适配(持续更新中) iOS13适配 掘金 ...

  • iOS 13适配

    技术参考: apple login IOS13适配-详细 iOS 13 适配(持续更新中) iOS13适配 掘金 ...

  • 暗黑模式开发

    iOS13暗黑模式适配(项目开发版) iOS 13 DarkMode 暗黑模式 IOS 暗黑模式适配---基础适配

  • iOS13适配更新总结

    前言: iOS13的API的变动和适配问题,我从新特性适配、API 适配、方法弃用、工程适配、SDK 适配、其他问...

  • iOS13适配研究

    iOS13今年秋季会发布,最近深入研究了下公司APP适配iOS13的注意点,适配如下。 1.由于Xcode10移除...

  • iOS13适配

    参考: iOS13 适配踩坑 - 持续更新 iOS 13 适配要点总结 iOS 13 适配要点总结 1、prese...

  • iOS13适配(更新中)

    对于iOS13适配汇总以及遇到的问题注意:以下适配内容,必须适配的会以"必须"标出 1. Dark Model(必...

  • 关于WRNavigationBar iOS12、iOS13导航栏

    集成WRNavigationBar 适配iOS12 iOS13导航栏问题 在修复iOS13下在iPhone11机型...

  • 关于WRNavigationBar iOS12、iOS13导航栏

    集成WRNavigationBar 适配iOS12 iOS13导航栏问题 在修复iOS13下在iPhone11机型...

  • 2019--09iOS13适配

    iOS13适配iOS13更新后对Ai定损、一车一件项目进行适配 做了一下调查 1 2 3 4 5 6 目前调研的只...

网友评论

      本文标题:iOS13 适配

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