Xcode11 和 iOS13 适配

一年一适配,今年又来了。
今年整体上问题不大,我司的App 没有出现编译报错问题。

首先,用 Xcode10 编译的 App 在 iOS 13 上使用甚至几乎完美😀
然而,用 Xcode11 编译的 App 再在 iOS 13 上跑,就有些问题了😂

具体碰到的问题如下:

modal新样式

iOS 13 多了一个新的枚举类型 UIModalPresentationAutomatic,且是modalPresentationStyle的默认值。

UIModalPresentationAutomatic实际是表现是在 =iOS 13的设备上被映射成UIModalPresentationPageSheet

我这边的设计师表示,新样式不错,可以不用改😆。

不过, PageSheetFullScreen 对比 有个需要注意的地方,控制器的生命周期有点区别

控制器A控制器B 举例:

  • 控制器A present 控制器B

    控制器A 不会调用 viewWillDisappear 以及 viewDidDisappear
  • 控制器B dismiss 时

    控制器A 不会调用 viewWillAppear 以及 viewDidAppear

那么如果有些业务逻辑会在控制器A的生命周期里做的话,就需要考虑其他方式实现,或者改回UIModalPresentationFullScreen

Xcode11_002

如果需要改成原本全屏的样式,可以处理Controller:

  • 初始化时设置 modalPresentationStyle
  • 跳转修改 modalPresentationStyle
  • 覆盖 modalPresentationStyle 的get方法

扩展
看上面gif,用户是可以通过手势下拉关闭被present出来的控制器的,那如果我需要禁止他下来要怎么实现呢?

可以参考 disabling_pulling_down_a_sheet 的Demo

设置presentationController.delegate 代理对象,实现 UIAdaptivePresentationControllerDelegate 协议方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@interface XXViewController () <UIAdaptivePresentationControllerDelegate>
@property (nonatomic, assign) BOOL allowBack;
@end

@implementation XXViewController
- (void)viewDidLoad {
[super viewDidLoad];
// ...

// 如果vc被navigation套了一层就要取navigation的presentationController
self.navigationController.presentationController.delegate = self;
}

// 使用UIAdaptivePresentationControllerDelegate 协议方法控制能否下拉dismiss
// 或者不实现这个协议,直接设置self.modalInPresentation = NO or YES;
- (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presentationController {
return self.allowBack;
}

// 如果 self.modalInPresentation=NO 或者
// 上面协议方法return NO,用户再下拉就会在这收到回调
- (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)presentationController {
// 如果allowBack == NO,会回调到这里
// 可以在这里做一些事情然后再考虑让allowBack = YES即可
}
@end

textfield.leftview

如下方式,直接给 textfield.leftView 赋值一个 UILabel 对象,他的宽高会被 sizeToFit,而不是创建时的值。

1
2
3
4
5
6
// left view label
UILabel *phoneLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 63, 50)];
phoneLabel.text = @"手机号";
phoneLabel.font = [UIFont systemFontOfSize:16];
// set textfield left view
self.textfieldName.leftView = phoneLabel;

如所看到,实际leftview的width为59,height为19:

Xcode11_003

通过监听 leftViewframe 变化,发现是 layoutSubview 之后变化的。
最终还是给UILabel多套了一个UIView来解决

1
2
3
4
5
6
7
8
9
10
11
// label
UILabel *phoneLabel = [[UILabel alloc] init];
phoneLabel.text = @"手机号";
phoneLabel.font = [UIFont systemFontOfSize:16];
[phoneLabel sizeToFit];
phoneLabel.centerY = 50/2.f;
// left view
UIView *leftView = [[UIView alloc] initWithFrame:(CGRect){0, 0, 63, 50}];
[leftView addSubview:phoneLabel];
// set textfield left view
self.textfieldName.leftView = leftView;

KVC访问私有属性Crash

打开有UISearchBar的页面发现Crash了,看到控制台输出提示:

1
2
3
4
5
6
7
// 获取_searchField
UITextField *sField = [self.searchbar valueForKey:@"_searchField"];

// crash log
*** Terminating app due to uncaught exception 'NSGenericException', reason:
'Access to UISearchBar's _searchField ivar is prohibited.
This is an application bug'

看起来是禁止访问私有属性了。

用 Xcode 10 编译的 App 在 iOS 13 上能正常使用,那么就是 Xcode 11 做了限制访问私有属性的一些处理了。

偶然发现 iOS 13 中增加了UISearchTextField类,且暴露了searchTextField

1
2
3
4
5
6
7
8
9
10
// UISearchTextField.h

UIKIT_CLASS_AVAILABLE_IOS_ONLY(13.0)
@interface UISearchTextField : UITextField
// ...
@end

@interface UISearchBar (UITokenSearch)
@property (nonatomic, readonly) UISearchTextField *searchTextField;
@end

但是仅在 iOS 13 以上系统支持,还是暂时用遍历view的方式去做了😂

点击导航栏返回的时候Crash了,控制台输出提示:

1
2
3
Теrmіnаtіng арр due to uncaught exception' NSInternalInconsistencyException' , 
reason : ' Override of -navigationBar : shouldPopItem: returned YES after
manually popping a view controller ( navigat ionController=<MHCRNavgationController : 0x106039400>) '

因为我们工程里,基本上所有的 Controller 是继承基类 BaseViewController 并实现- (BOOL)naviBack:方法,用于实现在用户点击返回和策划返回时,一些不能返回的特殊处理。
其根本原理是通过实现 UINavgationBar 的代理方法 - (BOOL)navigationBar:shouldPopItem: 来做的控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (BOOL)navigationBar:(UINavigationBar *)navigationBar
shouldPopItem:(UINavigationItem *)item {

// 默认可以返回
BOOL canGoBack = YES;

// BaseViewController定义协议方法判断能否能点击返回上一层
UIViewController *vc = self.viewControllers.lastObject;
if ([vc isKindOfClass:BaseViewController.class]) {
canGoBack = [(BaseViewController *)vc naviBack:nil];
}

if (canGoBack) {
[self popViewControllerAnimated:YES];
}
return canGoBack;
}

但是我实现的时候有 Return YES 啊!想了想,试着 注释[self popViewControllerAnimated:YES],发现没有崩溃了。
但是在iOS 12上,会发现控制器没有回到上一层,如图,只有navbar回到上一层了:

Xcode11_004

好吧,那只能判断一下版本解决这个问题了,修改方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (BOOL)navigationBar:(UINavigationBar *)navigationBar
shouldPopItem:(UINavigationItem *)item {

// 判断 iOS 版本低于13
BOOL bellow13 = !@available(iOS 13.0, *);

// 默认可以返回
BOOL canGoBack = YES;

// BaseViewController定义协议方法判断能否能点击返回上一层
UIViewController *vc = self.viewControllers.lastObject;
if ([vc isKindOfClass:BaseViewController.class]) {
canGoBack = [(BaseViewController *)vc naviBack:nil];
}

if (canGoBack && bellow13) {
// 如果低于13且可以返回,就执行popViewController
[self popViewControllerAnimated:YES];
}
return canGoBack;
}

夜间模式

WWDC 19 直播的时候看到夜间模式,老实说挺开心的,直到我用 Xcode 11 开始做适配,妈耶!x N

注意:使用 Xcode 10 编译的 App 依然是日间模式,不会产生效果!!!

初步扫了一下出现的问题如下图,大致情况是:没有设置背景色的系统控件会被设置成黑色,部分控件是tintColor没设置的话也会被改。

Xcode11_005

Xcode11_006

由于Assets里的Color配置是 iOS 12 以上才能使用的,所以如果没有做全局主题色设计且需要支持iOS 12以下设备,改起来会比较恶心。

对此现象,找设计师沟通。设计师表示,暂时没有精力做夜间模式规划。
设计师问:能否强制只日间模式?
答:能。配置方式有两种,单页面配置全局配置

  • 单页配置
    将需要配置的 UIViewControler 对象的 overrideUserInterfaceStyle 属性设置成 UIUserInterfaceStyleLight 或者 UIUserInterfaceStyleDark 以强制是某个页面显示为 浅/深色模式

  • 全局配置
    在工程的Info.plist的中,增加/修改 UIUserInterfaceStyleUIUserInterfaceStyleLightUIUserInterfaceStyleDark

2019.07.26 补充:
早上刚好看到知识小集发了篇 适配 Dark Mode 的文章,看了下挺好的,推荐阅读。

使用 QMUITheme 实现换肤并适配 iOS 13 Dark Mode

Pod私有库校验报错找不到simulator

推 Cocoapods 私有库校验的时候,收到一个报错:

1
2
3
- ERROR | [iOS] unknown: Encountered an unknown error (Could not find aiossimulator 
(valid values: ). Ensure that Xcode -> Window -> Devices has at least
oneiossimulator listed or otherwise add one.) during validation.

直觉告诉我是 cocoapods 的问题,去其 github 仓库的 issues 搜了一下,没有搜到 xcode 11 相关的,然后去 google 了一下,看到了这个问题:

cocoapods-cant-find-simulators-pod-repo-push-fails

里面有一条

After installing Xcode 11, pod lib lint is broken again because of fourflusher -
The “availability” key from xcrun simctl list -j is now a boolean “isAvailable”

但是这个fourflusher的路径我找不到,可能自己的ruby环境不太一样,那么gem会不会支持反查一个工具的位置呢?

通过 gem help 看了下使用说明,发现可以用gem help commands查找所有gem的指令。
其中就有一个which指令,描述上说是用来找库文件位置的。

1
2
# 使用gem which来查找位置
gem which fourflusher

果然出现了:

1
/Users/HY/.rvm/gems/ruby-2.4.1/gems/fourflusher-2.2.0/lib/fourflusher.rb

对应的知道 fourflusher 的位置,那么接下来就可以修改了。

  1. 前往 /Users/HY/.rvm/gems/ruby-2.4.1/gems/fourflusher-2.2.0/lib/fourflusher/
  2. 打开文件find.rb
  3. 查找 device['availability'] == '(available)'
  4. 增加一个判断 device['isAvailable'] == true 即可

修改结果如图即可:
Xcode11_006