时间:2021-12-10 15:59:44 | 栏目:iOS代码 | 点击:次
随着iOS 13的发布,公司的项目也势必要着手适配了。现汇总一下iOS 13的各种坑
这次iOS 13系统升级,影响范围最广的应属KVC访问修改私有属性了,直接禁止开发者获取或直接设置私有属性。而KVC的初衷是允许开发者通过Key名直接访问修改对象的属性值,为其中最典型的 UITextField 的 _placeholderLabel、UISearchBar 的 _searchField。
造成影响:在iOS 13下App闪退
错误代码:
// 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 的 _placeholderLabel
textField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:@"placeholder" attributes:@{NSForegroundColorAttributeName: [UIColor darkGrayColor], NSFontAttributeName: [UIFont systemFontOfSize:13]}];
因此,可以为UITextFeild创建Category,专门用于处理修改placeHolder属性提供方法
#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 的通过富文本方法修改属性。
#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 文章
模态弹窗属性 UIModalPresentationStyle 在 iOS 13 下默认被设置为 UIModalPresentationAutomatic新特性,展示样式更为炫酷,同时可用下拉手势关闭模态弹窗。
若原有模态弹出 ViewController 时都已指定模态弹窗属性,则可以无视该改动。
若想在 iOS 13 中继续保持原有默认模态弹窗效果。可以通过 runtime 的 Method Swizzling 方法交换来实现。
#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
针对黑暗模式的推出,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 11、iOS 13开始,UIViewController与View新增属性 overrideUserInterfaceStyle,若设置View对象该属性为指定模式,则强制该对象以及子对象以指定模式展示,不会跟随系统模式改变。
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;
使用 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 如下图,完成
在iOS13之前,无需权限提示窗即可直接使用蓝牙,但在iOS 13下,新增了使用蓝牙的权限申请。最近一段时间上传IPA包至App Store会收到以下提示。
解决方案:只需要在 Info.plist 里增加以下条目:
<key>NSBluetoothAlwaysUsageDescription</key> <string>这里输入使用蓝牙来做什么</string>`
在iOS 13系统中,Apple要求提供第三方登录的App也要支持「Sign With Apple」,具体实践参考 iOS Sign With Apple实践
在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; }
主要还是参照了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); }
8.3. 新增 Diffable DataSource
在 iOS 13下,对 UITableView 与 UICollectionView 新增了一套 Diffable DataSource API。为了更高效地更新数据源刷新列表,避免了原有粗暴的刷新方法 - (void)reloadData,以及手动调用控制列表刷新范围的api,很容易出现计算不准确造成 NSInternalInconsistencyException 而引发App crash。
api 官方链接
StatusBar 新增一种样式,默认的 default 由之前的黑色字体,变为根据系统模式自动选择展示 lightContent 或者 darkContent
针对iOS 13 SDK适配,后续将会持续收集并更新