iOS 13适配笔记

1. 获取状态栏

项目中有通过StatusBar来获取手机当前状态,但是在iOS 13中会崩溃,调试了一下发现是通过KVC获取UIApplicationstatusBar属性造成的。iOS13新增加了UIStatusBarManager相关的类,可以通过获取程序window进而获取到UIStatusBarManager。

然后通过Runtime获取到UIStatusBarManager的属性列表及方法列表:

properties is: {
"_scene" = "<UIWindowScene: 0x7f9505407e70; scene = <FBSSceneImpl: 0x6000021f8680; identifier: sceneID:PsychokinesisTeam.iOS-Test-default>; persistentIdentifier = 392A053A-1C6B-4A42-AB98-59AB3ADFB290; activationState = UISceneActivationStateForegroundActive; settingsCanvas = <UIWindowScene: 0x7f9505407e70>; windows = (\n \"<UIWindow: 0x7f9505411180; frame = (0 0; 414 896); hidden = YES; gestureRecognizers = <NSArray: 0x6000001a5f20>; layer = <UIWindowLayer: 0x600000ff5ae0>>\",\n \"<UIWindow: 0x7f95054066a0; frame = (0 0; 414 896); gestureRecognizers = <NSArray: 0x600000192280>; layer = <UIWindowLayer: 0x600000fd9f80>>\",\n \"<UIWindow: 0x7f950541f400; frame = (0 0; 414 896); hidden = YES; gestureRecognizers = <NSArray: 0x600000199590>; layer = <UIWindowLayer: 0x600000fdb7c0>>\",\n \"<UITextEffectsWindow: 0x7f950550b3a0; frame = (0 0; 414 896); opaque = NO; autoresize = W+H; layer = <UIWindowLayer: 0x600000fac380>>\"\n)>";
debugDescription = "<UIStatusBarManager: 0x600001a92900>";
debugMenuHandler = "<null>";
description = "<UIStatusBarManager: 0x600001a92900>";
hash = 105553144129792;
inStatusBarFadeAnimation = 0;
localStatusBars = "<null>";
statusBarAlpha = 1;
statusBarFrame = "NSRect: {{0, 0}, {414, 44}}";
statusBarHeight = 44;
statusBarHidden = 0;
statusBarPartStyles = {
leadingPartIdentifier = 4;
trailingPartIdentifier = 4;
};
statusBarStyle = 3;
superclass = NSObject;
windowScene = "<UIWindowScene: 0x7f9505407e70; scene = <FBSSceneImpl: 0x6000021f8680; identifier: sceneID:PsychokinesisTeam.iOS-Test-default>; persistentIdentifier = 392A053A-1C6B-4A42-AB98-59AB3ADFB290; activationState = UISceneActivationStateForegroundActive; settingsCanvas = <UIWindowScene: 0x7f9505407e70>; windows = (\n \"<UIWindow: 0x7f9505411180; frame = (0 0; 414 896); hidden = YES; gestureRecognizers = <NSArray: 0x6000001a5f20>; layer = <UIWindowLayer: 0x600000ff5ae0>>\",\n \"<UIWindow: 0x7f95054066a0; frame = (0 0; 414 896); gestureRecognizers = <NSArray: 0x600000192280>; layer = <UIWindowLayer: 0x600000fd9f80>>\",\n \"<UIWindow: 0x7f950541f400; frame = (0 0; 414 896); hidden = YES; gestureRecognizers = <NSArray: 0x600000199590>; layer = <UIWindowLayer: 0x600000fdb7c0>>\",\n \"<UITextEffectsWindow: 0x7f950550b3a0; frame = (0 0; 414 896); opaque = NO; autoresize = W+H; layer = <UIWindowLayer: 0x600000fac380>>\"\n)>";
}
methods is: (
".cxx_destruct",
windowScene,
statusBarFrame,
"_setScene:",
"initWithScene:",
"_scene",
"_settingsDiffActionsForScene:",
"setWindowScene:",
isStatusBarHidden,
statusBarStyle,
statusBarHeight,
"setDebugMenuHandler:",
"handleTapAction:",
"_updateAlpha",
statusBarHidden,
statusBarPartStyles,
statusBarAlpha,
"defaultStatusBarHeightInOrientation:",
updateStatusBarAppearance,
updateLocalStatusBars,
setupForSingleLocalStatusBar,
"statusBarFrameForStatusBarHeight:",
"updateStatusBarAppearanceWithAnimationParameters:",
"_updateStatusBarAppearanceWithClientSettings:transitionContext:animationParameters:",
"_updateVisibilityForWindow:targetOrientation:animationParameters:",
"_updateStyleForWindow:animationParameters:",
"_visibilityChangedWithOriginalOrientation:targetOrientation:animationParameters:",
"activateLocalStatusBar:",
"_updateLocalStatusBar:",
"_handleScrollToTopAtXPosition:",
"_adjustedLocationForXPosition:",
"updateStatusBarAppearanceWithClientSettings:transitionContext:",
"deactivateLocalStatusBar:",
createLocalStatusBar,
localStatusBars,
"setLocalStatusBars:",
isInStatusBarFadeAnimation,
debugMenuHandler
)

可以看到有几个与statusBar相关的属性和方法,最总尝试了很多次,只有通过createLocalStatusBar可以获取到_UIStatusBarLocalView

<_UIStatusBarLocalView: 0x7fe86261d780; frame = (0 0; 414 44); layer = <CALayer: 0x600000c3b4c0>>

然后通过KVC进而获取到UIStatusBar_Modern

[obj valueForKeyPath:@"statusBar"];
<UIStatusBar_Modern: 0x7fe862702cf0; frame = (0 0; 414 44); autoresize = W+H; layer = <CALayer: 0x600000c50780>>

最终拿到了statusBar,详细代码如下:

+ (nullable UIView *)getUIStatusBar {
if (@available(iOS 13.0, *)) {
UIStatusBarManager *statusBarManager = [UIApplication sharedApplication].keyWindow.windowScene.statusBarManager;
SEL aSelector = NSSelectorFromString(@"createLocalStatusBar");
if ([statusBarManager respondsToSelector:aSelector]) {
UIView *_localView = ((id(*)(id, SEL))objc_msgSend)(statusBarManager, aSelector);
return [_localView valueForKeyPath:@"statusBar"];
}
return nil;
} else {
UIApplication *application = [UIApplication sharedApplication];
return [application valueForKeyPath:@"statusBar"];
}
}

通过上述方法运行后获取statusBar再次出现崩溃,查看到相关信息如下:

- [UIView setAnimationsEnabled:] being called from a background thread. Performing any operation from a background thread on UIView or a subclass is not supported and may result in unexpected and insidious behavior.
- This application is modifying the autolayout engine from a background thread after the engine was accessed from the main thread. This can lead to engine corruption and weird crashes.

猜想可能是调用createLocalStatusBar方法时有UI的创建或者动画,而此时的调用可能是在子线程里操作的,所以增加了线程判断再去获取statusBar,最终解决问题:

__block id statusBar = nil;
if ([NSThread.currentThread isMainThread]) {
statusBar = [self getUIStatusBar];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
statusBar = [self getUIStatusBar];
});
}

以上方法调用了一些系统私有方法,还不晓得上线审核是否会被拒。

2. 暗黑模式适配

  • 全局关闭暗黑模式
  1. 在Info.plist 文件中,添加UIUserInterfaceStyle key为 User Interface Style 类型为String,
  2. UIUserInterfaceStyle key 的值设置为 Light
  • 单个界面不遵循暗黑模式
  1. UIViewController与UIView 都新增了一个属性 overrideUserInterfaceStyle

  2. overrideUserInterfaceStyle

    设置为对应的模式,则强制限制该元素与其子元素以设置的模式进行展示,不跟随系统模式改变进行改变

    1. 设置 ViewController 的该属性,将会影响视图控制器的视图和子视图控制器采用该样式
    2. 设置 View 的该属性,将会影响视图及其所有子视图采用该样式
    3. 设置 Window 的该属性, 将会影响窗口中的所有内容都采用样式,包括根视图控制器和在该窗口中显示内容的所有演示控制器(UIPresentationController)
  • 适配暗黑模式

目前全局禁止了暗黑模式,部分页面还需要等UI设计下暗黑模式的效果。

  1. TODO

暗黑模式的适配可以参考这里

3. UITextField设置placeholder颜色

[textField setValue:aColor forKeyPath:@"_placeholderLabel.textColor"];

iOS 13中通过KVC设置占位文本颜色会崩溃,详细信息如下:

*** Terminating app due to uncaught exception 'NSGenericException', reason: 'Access to UITextField's _placeholderLabel ivar is prohibited. This is an application bug'

提示该方法已经被禁止使用,可以改成使用attributedPlaceholder来设置:

NSString *placeholderText = @"请输入...";
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:placeholderText];
[attributedText addAttribute:NSForegroundColorAttributeName value:[UIColor lightGrayColor] range:NSMakeRange(0, placeholderText.length)];
self.textField.attributedPlaceholder = attributedText;

上面的代码比较多,如果项目中使用KVC方式赋值颜色的地方比较多,修改后的代码会比较冗余。为此我为UITextField写了一个分类,不用写其它多余代码,一行代码即可搞定,在老代码KVC赋值的地方相应替换为:

// [textField setValue:aColor forKeyPath:@"_placeholderLabel.textColor"]; 替换成下面这行代码
textField.pk_placeholderColor = aColor;

分类详细代码如下:

@interface UITextField (PKPlaceholder)
/** 设置占位文本颜色 */
@property (nonatomic, strong, nullable) UIColor *pk_placeholderColor;
@end
static void *UITextFieldAssociatedPKPlaceholderKey = &UITextFieldAssociatedPKPlaceholderKey;
@implementation UITextField (PKPlaceholder)
- (UIColor *)pk_placeholderColor {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setPk_placeholderColor:(UIColor *)pk_placeholderColor {
NSString *placeholder = self.placeholder;
if (placeholder) {
NSMutableAttributedString *attriText = [[NSMutableAttributedString alloc] initWithString:placeholder];
[attriText addAttribute:NSForegroundColorAttributeName value:pk_placeholderColor range:NSMakeRange(0, placeholder.length)];
self.attributedPlaceholder = attriText;
}
objc_setAssociatedObject(self, @selector(pk_placeholderColor), pk_placeholderColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
- (NSString *)placeholder {
NSString *placeholderText = objc_getAssociatedObject(self, UITextFieldAssociatedPKPlaceholderKey);
if (placeholderText) return placeholderText;
return self.attributedPlaceholder.string;
}
- (void)setPlaceholder:(NSString *)placeholder {
objc_setAssociatedObject(self, UITextFieldAssociatedPKPlaceholderKey, placeholder, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
UIColor *placeholderClolor = objc_getAssociatedObject(self, @selector(pk_placeholderColor));
if (placeholderClolor && placeholder) {
self.attributedPlaceholder = [[NSMutableAttributedString alloc] initWithString:placeholder];
[self setPk_placeholderColor:placeholderClolor];
}
}
#pragma clang diagnostic pop
@end

4. UITextField的leftView和内边距调整

之前项目通过设置leftView来达到设置UITextField内边距的效果,在iOS 13版本上失效,原因是在iOS 13中如果使用UIImageView来设置leftView,则必须为其设置一个有效的image,否则将无法按照意愿布局。

// iOS 13中使用空UIImageView设置leftView则布局失败
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)];
textField.leftView = imageView;

解决方案,改为UIView或者把UIImageVIew 包一层UIView再设置给leftView,且不要使用约束布局。

// iOS 13中直接使用UIView设置leftView则布局成功
UIView *aView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)];
textField.leftView = aView;

不过,以上方案设置UITextField的内边距有很大的局限性,例如我想使用图片来设置一个真正可见的leftView,并且还想调整内边距,显然这种方法无法满足需求,为此我为UITextField写了个分类,简单一行代码即可设置UITextField的文本和占位符的内边距:

@interface UITextField (PKExtend)
/** 调整文本内边距 */
@property (nonatomic, assign) UIEdgeInsets pk_textEdgeInsets;
/** 调整占位符文本内边距 */
@property (nonatomic, assign) UIEdgeInsets pk_placeHolderEdgeInsets;
@end
@implementation UITextField (PKExtend)
+ (void)load {
SEL selectors[] = {
@selector(placeholderRectForBounds:),
@selector(textRectForBounds:),
@selector(editingRectForBounds:)
};
for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); ++index) {
SEL originalSelector = selectors[index];
SEL swizzledSelector = NSSelectorFromString([@"_pk_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
- (CGRect)_pk_placeholderRectForBounds:(CGRect)bounds {
[self _pk_placeholderRectForBounds:bounds];
CGRect rect = [self _pk_textRectForBounds:bounds];
return UIEdgeInsetsInsetRect(rect, self.pk_placeHolderEdgeInsets);
}
- (CGRect)_pk_textRectForBounds:(CGRect)bounds {
CGRect rect = [self _pk_textRectForBounds:bounds];
return UIEdgeInsetsInsetRect(rect, self.pk_textEdgeInsets);
}
- (CGRect)_pk_editingRectForBounds:(CGRect)bounds {
CGRect rect = [self _pk_textRectForBounds:bounds];
return UIEdgeInsetsInsetRect(rect, self.pk_textEdgeInsets);
}
- (UIEdgeInsets)pk_textEdgeInsets {
return [objc_getAssociatedObject(self, _cmd) UIEdgeInsetsValue];
}
- (void)setPk_textEdgeInsets:(UIEdgeInsets)pk_textEdgeInsets {
NSValue *value = [NSValue valueWithUIEdgeInsets:pk_textEdgeInsets];
objc_setAssociatedObject(self, @selector(pk_textEdgeInsets), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIEdgeInsets)pk_placeHolderEdgeInsets {
return [objc_getAssociatedObject(self, _cmd) UIEdgeInsetsValue];
}
- (void)setPk_placeHolderEdgeInsets:(UIEdgeInsets)pk_placeHolderEdgeInsets {
NSValue *value = [NSValue valueWithUIEdgeInsets:pk_placeHolderEdgeInsets];
objc_setAssociatedObject(self, @selector(pk_placeHolderEdgeInsets), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end

5. 模态跳转ModalPresent

iOS 13系统下修改了模态跳转样式,苹果可能是想与push页面有侧滑手势一样,增加了下滑手势,个人感觉体验不错,交互样式也有提升,不用再自己手写转场动画了。当然如果想改成老样式,可以手动改成UIModalPresentationOverFullScreen,不过个人推荐使用iOS 13新增加的枚举值UIModalPresentationAutomatic,使用系统推荐的模态样式:

NextViewController *vc = [NextViewController new];
if (@available(iOS 13.0, *)) {
vc.modalPresentationStyle = UIModalPresentationAutomatic;
} else {
vc.modalPresentationStyle = UIModalPresentationOverFullScreen;
}
[self presentViewController:vc animated:YES completion:NULL];

默认情况下不设置即是pageSheet样式,当然除 UISearchControllerUIAlertController 因自身业务的特殊性不同外,其余的视图控制器均为此展示样式。

如果项目中禁止了全局暗黑模式,但还想使用pageSheet样式,还要记得配置下window的颜色,不然弹出后window背景色是白色看不出分层次效果:

self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
if (@available(iOS 13.0, *)) {
self.window.backgroundColor = [UIColor systemBackgroundColor];
} else {
self.window.backgroundColor = [UIColor whiteColor];
}
self.window.rootViewController = self.rootVC;
[self.window makeKeyAndVisible];

6. 关于_LSDefaults崩溃问题

程序运行后可能会出现_LSDefaults崩溃:

+[_LSDefaults sharedInstance]: unrecognized selector sent to class...

可能是友盟SDK版本问题导致,升级后即可正常运行。

还有在网上看到的解决方案是通过拦截系统的doesNotRecognizeSelector:方法,强行终止其抛出异常,具体实现可以参考这里。虽然理论上可以拦截或重载这个函数实现不抛出异常,但是苹果文档则建议是“一定不能让这个函数就这么结束掉,必须抛出异常”。

热评文章