iOS 13原生端适配攻略

随着iOS 13的发布,公司的项目也势必要着手适配了。现汇总一下iOS 13的各类坑html

目录

1. KVC访问私有属性

2. 模态弹窗ViewController 默认样式改变

3. 黑暗模式的适配

4. LaunchImage即将废弃

5. 新增一直使用蓝牙的权限申请

6. Sign With Apple

7. 推送Device Token适配

8. UIKit 控件变化

9. StatusBar新增样式


1. KVC访问私有属性

 此次iOS 13系统升级,影响范围最广的应属KVC访问修改私有属性了,直接禁止开发者获取或直接设置私有属性。而KVC的初衷是容许开发者经过Key名直接访问修改对象的属性值,为其中最典型的 UITextField_placeholderLabelUISearchBar_searchField。 形成影响:在iOS 13下App闪退 错误代码:ios

// placeholderLabel私有属性访问
[textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
[textField setValue:[UIFont boldSystemFontOfSize:16] forKeyPath:@"_placeholderLabel.font"];
// searchField私有属性访问
UISearchBar *searchBar = [[UISearchBar alloc] init];
UITextField *searchTextField = [searchBar valueForKey:@"_searchField"];
复制代码

解决方案:  使用 NSMutableAttributedString 富文原本替代KVC访问 UITextField_placeholderLabelapi

textField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:@"placeholder" attributes:@{NSForegroundColorAttributeName: [UIColor darkGrayColor], NSFontAttributeName: [UIFont systemFontOfSize:13]}];
复制代码

 所以,能够为UITextFeild建立Category,专门用于处理修改placeHolder属性提供方法xcode

#import "UITextField+ChangePlaceholder.h"

@implementation UITextField (Change)

- (void)setPlaceholderFont:(UIFont *)font {

  [self setPlaceholderColor:nil font:font];
}

- (void)setPlaceholderColor:(UIColor *)color {

  [self setPlaceholderColor:color font:nil];
}

- (void)setPlaceholderColor:(nullable UIColor *)color font:(nullable UIFont *)font {

  if ([self checkPlaceholderEmpty]) {
      return;
  }

  NSMutableAttributedString *placeholderAttriString = [[NSMutableAttributedString alloc] initWithString:self.placeholder];
  if (color) {
      [placeholderAttriString addAttribute:NSForegroundColorAttributeName value:color range:NSMakeRange(0, self.placeholder.length)];
  }
  if (font) {
      [placeholderAttriString addAttribute:NSFontAttributeName value:font range:NSMakeRange(0, self.placeholder.length)];
  }

  [self setAttributedPlaceholder:placeholderAttriString];
}

- (BOOL)checkPlaceholderEmpty {
  return (self.placeholder == nil) || ([[self.placeholder stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length] == 0);
}
复制代码

 关于 UISearchBar,可遍历其全部子视图,找到指定的 UITextField 类型的子视图,再根据上述 UITextField 的经过富文本方法修改属性。bash

#import "UISearchBar+ChangePrivateTextFieldSubview.h"

@implementation UISearchBar (ChangePrivateTextFieldSubview)

/// 修改SearchBar系统自带的TextField
- (void)changeSearchTextFieldWithCompletionBlock:(void(^)(UITextField *textField))completionBlock {

    if (!completionBlock) {
        return;
    }
    UITextField *textField = [self findTextFieldWithView:self];
    if (textField) {
        completionBlock(textField);
    }
}

/// 递归遍历UISearchBar的子视图,找到UITextField
- (UITextField *)findTextFieldWithView:(UIView *)view {

    for (UIView *subview in view.subviews) {
        if ([subview isKindOfClass:[UITextField class]]) {
            return (UITextField *)subview;
        }else if (subview.subviews.count > 0) {
            return [self findTextFieldWithView:subview];
        }
    }
    return nil;
}
@end
复制代码

 PS:关于如何查找本身的App项目是否使用了私有api,能够参考 iOS查找私有API 文章app


2. 模态弹窗 ViewController 默认样式改变

 模态弹窗属性 UIModalPresentationStyle 在 iOS 13 下默认被设置为 UIModalPresentationAutomatic新特性,展现样式更为炫酷,同时可用下拉手势关闭模态弹窗。 若原有模态弹出 ViewController 时都已指定模态弹窗属性,则能够无视该改动。 若想在 iOS 13 中继续保持原有默认模态弹窗效果。能够经过 runtime 的 Method Swizzling 方法交换来实现。ide

#import "UIViewController+ChangeDefaultPresentStyle.h"

@implementation UIViewController (ChangeDefaultPresentStyle)

+ (void)load {

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        //替换方法
        SEL originalSelector = @selector(presentViewController:animated:completion:);
        SEL newSelector = @selector(new_presentViewController:animated:completion:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method newMethod = class_getInstanceMethod(class, newSelector);;
        BOOL didAddMethod =
        class_addMethod(class,
                        originalSelector,
                        method_getImplementation(newMethod),
                        method_getTypeEncoding(newMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                                newSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));

        } else {
            method_exchangeImplementations(originalMethod, newMethod);
        }
    });
}

- (void)new_presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion {

    viewControllerToPresent.modalPresentationStyle = UIModalPresentationFullScreen;
    [self new_presentViewController:viewControllerToPresent animated:flag completion:completion];
}

@end
复制代码

3. 黑暗模式的适配

 针对黑暗模式的推出,Apple官方推荐全部三方App尽快适配。目前并无强制App进行黑暗模式适配。所以黑暗模式适配范围如今可采用如下三种策略:函数

  • 全局关闭黑暗模式
  • 指定页面关闭黑暗模式
  • 全局适配黑暗模式

3.1. 全局关闭黑暗模式

 方案一:在项目 Info.plist 文件中,添加一条内容,Key为 User Interface Style,值类型设置为String并设置为 Light 便可。工具

 方案二:代码强制关闭黑暗模式,将当前 window 设置为 Light 状态。字体

if(@available(iOS 13.0,*)){
self.window.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
}
复制代码

3.2 指定页面关闭黑暗模式

 从Xcode 十一、iOS 13开始,UIViewController与View新增属性 overrideUserInterfaceStyle,若设置View对象该属性为指定模式,则强制该对象以及子对象以指定模式展现,不会跟随系统模式改变。

  • 设置 ViewController 该属性, 将会影响视图控制器的视图以及子视图控制器都采用该模式
  • 设置 View 该属性, 将会影响视图及其全部子视图采用该模式
  • 设置 Window 该属性, 将会影响窗口中的全部内容都采用该样式,包括根视图控制器和在该窗口中显示内容的全部控制器

3.3 全局适配黑暗模式

 适配黑暗模式,主要从两方面入手:图片资源适配与颜色适配

图片资源适配

 打开图片资源管理库 Assets.xcassets,选中须要适配的图片素材item,打开最右侧的 Inspectors 工具栏,找到 Appearances 选项,并设置为 Any, Dark模式,此时会在item下增长Dark Appearance,将黑暗模式下的素材拖入便可。关于黑暗模式图片资源的加载,与正常加载图片方法一致。

图片资源适配黑暗模式

颜色适配

 iOS 13开始UIColor变为动态颜色,在Light Mode与Dark Mode能够分别设置不一样颜色。 若UIColor色值管理,与图片资源同样存储于 Assets.xcassets 中,一样参照上述方法适配。 若UIColor色值并无存储于 Assets.xcassets 状况下,自定义动态UIColor时,在iOS 13下初始化方法增长了两个方法

+ (UIColor *)colorWithDynamicProvider:(UIColor * (^)(UITraitCollection *))dynamicProvider API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
- (UIColor *)initWithDynamicProvider:(UIColor * (^)(UITraitCollection *))dynamicProvider API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
复制代码
  • 这两个方法要求传一个block,block会返回一个 UITraitCollection 类
  • 当系统在黑暗模式与正常模式切换时,会触发block回调 示例代码:
UIColor *dynamicColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull trainCollection) {
        if ([trainCollection userInterfaceStyle] == UIUserInterfaceStyleLight) {
            return [UIColor whiteColor];
        } else {
            return [UIColor blackColor];
        }
    }];
    
 [self.view setBackgroundColor:dynamicColor];
复制代码

 固然了,iOS 13系统也默认提供了一套基本的黑暗模式UIColor动态颜色,具体声明以下:

@property (class, nonatomic, readonly) UIColor *systemBrownColor        API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemIndigoColor       API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemGray2Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGray3Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGray4Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGray5Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGray6Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *labelColor              API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *secondaryLabelColor     API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *tertiaryLabelColor      API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *quaternaryLabelColor    API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *linkColor               API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *placeholderTextColor    API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *separatorColor          API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *opaqueSeparatorColor    API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemBackgroundColor                   API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *secondarySystemBackgroundColor          API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *tertiarySystemBackgroundColor           API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGroupedBackgroundColor            API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *secondarySystemGroupedBackgroundColor   API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *tertiarySystemGroupedBackgroundColor    API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemFillColor                         API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *secondarySystemFillColor                API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *tertiarySystemFillColor                 API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *quaternarySystemFillColor               API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
复制代码

监听模式的切换

 当须要监听系统模式发生变化并做出响应时,须要用到 ViewController 如下函数

// 注意:参数为变化前的traitCollection,改函数须要重写
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection;
 
// 判断两个UITraitCollection对象是否不一样
- (BOOL)hasDifferentColorAppearanceComparedToTraitCollection:(UITraitCollection *)traitCollection;
复制代码

示例代码:

- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
    [super traitCollectionDidChange:previousTraitCollection];
    // trait has Changed?
    if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
    // do something...
    }
    }
复制代码

系统模式变动,自定义重绘视图

 当系统模式变动时,系统会通知全部的 View以及 ViewController 须要更新样式,会触发如下方法执行(参考Apple官方适配连接):

NSView

- (void)updateLayer;
- (void)drawRect:(NSRect)dirtyRect;
- (void)layout;
- (void)updateConstraints;
复制代码

UIView

- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection;
- (void)layoutSubviews;
- (void)drawRect:(NSRect)dirtyRect;
- (void)updateConstraints;
- (void)tintColorDidChange;
复制代码

UIViewController

- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection;
- (void)updateViewConstraints;
- (void)viewWillLayoutSubviews;
- (void)viewDidLayoutSubviews;
复制代码

UIPresentationController

- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection;
- (void)containerViewWillLayoutSubviews;
- (void)containerViewDidLayoutSubviews;
复制代码

4. LaunchImage即将废弃

 使用 LaunchImage 设置启动图,须要提供各种屏幕尺寸的启动图适配,这种方式随着各种设备尺寸的增长,增长了额外没必要要的工做量。为了解决 LaunchImage 带来的弊端,iOS 8引入了 LaunchScreen 技术,由于支持 AutoLayout + SizeClass,因此经过 LaunchScreen 就能够简单解决适配当下以及将来各类屏幕尺寸。 Apple官方已经发出公告,2020年4月开始,全部使用iOS 13 SDK 的App都必须提供 LaunchScreen。 建立一个 LaunchScreen 也很是简单 (1)New Files建立一个 LaunchScreen,在建立的 ViewController 下 View 中新建一个 Image,并配置 Image 的图片 (2)调整 Image 的 frame 为占满屏幕,并修改 Image 的 Autoresizing 以下图,完成

Image 的 Autoresizing 配置

5. 新增一直使用蓝牙的权限申请

 在iOS13以前,无需权限提示窗便可直接使用蓝牙,但在iOS 13下,新增了使用蓝牙的权限申请。最近一段时间上传IPA包至App Store会收到如下提示。

解决方案:只须要在 Info.plist 里增长如下条目:

<key>NSBluetoothAlwaysUsageDescription</key> 
<string>这里输入使用蓝牙来作什么</string>`
复制代码

6. Sign With Apple

 在iOS 13系统中,Apple要求提供第三方登陆的App也要支持「Sign With Apple」,具体实践参考 iOS Sign With Apple实践


7. 推送Device Token适配

 在iOS 13以前,获取Device Token 是将系统返回的 NSData 类型数据经过 -(void)description; 方法直接转换成 NSString 字符串。 iOS 13以前获取结果:

iOS 13以后获取结果:
 适配方案: 目的是要将系统返回 NSData 类型数据转换成字符串,再传给推送服务方。 -(void)description; 自己是用于为类调试提供相关的打印信息,严格来讲,不该直接从该方法获取数据并应用于正式环境中。将 NSData 转换成 HexString,便可知足适配需求。

- (NSString *)getHexStringForData:(NSData *)data {
    NSUInteger length = [data length];
    char *chars = (char *)[data bytes];
    NSMutableString *hexString = [[NSMutableString alloc] init];
    for (NSUInteger i = 0; i < length; i++) {
        [hexString appendString:[NSString stringWithFormat:@"%0.2hhx", chars[i]]];
    }
    return hexString;
}
复制代码

8. UIKit 控件变化

 主要仍是参照了Apple官方的 UIKit 修改文档声明。iOS 13 Release Notes

8.1. UITableView

 iOS 13下设置 cell.contentView.backgroundColor 会直接影响 cell 自己 selected 与 highlighted 效果。 建议不要对 contentView.backgroundColor 修改,而对 cell 自己进行设置。

8.2. UITabbar

Badge 文字大小变化

 iOS 13以后,Badge 字体默认由13号变为17号。 建议在初始化 TabbarController 时,显示 Badge 的 ViewController 调用 setBadgeTextAttributes:forState: 方法

if (@available(iOS 13, *)) {
    [viewController.tabBarItem setBadgeTextAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:13]} forState:UIControlStateNormal];
    [viewController.tabBarItem setBadgeTextAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:13]} forState:UIControlStateSelected];
}
复制代码

8.2. UITabBarItem

加载gif需设置 scale 比例

NSData *data = [NSData dataWithContentsOfFile:path];
CGImageSourceRef gifSource = CGImageSourceCreateWithData(CFBridgingRetain(data), nil);
size_t gifCount = CGImageSourceGetCount(gifSource);
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(gifSource, i,NULL);

//  iOS 13以前
UIImage *image = [UIImage imageWithCGImage:imageRef]
//  iOS 13以后添加scale比例(该imageView将展现该动图效果)
UIImage *image = [UIImage imageWithCGImage:imageRef scale:image.size.width / CGRectGetWidth(imageView.frame) orientation:UIImageOrientationUp];

CGImageRelease(imageRef);
复制代码

无文字时图片位置调整

 iOS 13下不须要调整 imageInsets,图片会自动居中显示,所以只须要针对iOS 13以前的作适配便可。

if (IOS_VERSION < 13.0) {
      viewController.tabBarItem.imageInsets = UIEdgeInsetsMake(5, 0, -5, 0);
  }
复制代码

TabBarItem选中颜色异常

 在 iOS 13下设置 tabbarItem 字体选中状态的颜色,在push到其它 ViewController 再返回时,选中状态的 tabbarItem 颜色会变成默认的蓝色。

设置 tabbar 的 tintColor 属性为本来选中状态的颜色便可。

self.tabBar.tintColor = [UIColor redColor];
复制代码

8.3. 新增 Diffable DataSource

 在 iOS 13下,对 UITableView 与 UICollectionView 新增了一套 Diffable DataSource API。为了更高效地更新数据源刷新列表,避免了原有粗暴的刷新方法 - (void)reloadData,以及手动调用控制列表刷新范围的api,很容易出现计算不许确形成 NSInternalInconsistencyException 而引起App crash。 api 官方连接


9. StatusBar新增样式

 StatusBar 新增一种样式,默认的 default 由以前的黑色字体,变为根据系统模式自动选择展现 lightContent 或者 darkContent

针对iOS 13 SDK适配,后续将会持续收集并更新

相关文章
相关标签/搜索