美文网首页
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 适配

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