iOS13原生端适配攻略(推荐)

时间:2022-03-03 07:00:28

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

1. kvc访问私有属性

 

这次ios 13系统升级,影响范围最广的应属kvc访问修改私有属性了,直接禁止开发者获取或直接设置私有属性。而kvc的初衷是允许开发者通过key名直接访问修改对象的属性值,为其中最典型的 uitextfield 的 _placeholderlabel、uisearchbar 的 _searchfield。

造成影响:在ios 13下app闪退

错误代码:

?
1
2
3
4
5
6
// 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

?
1
textfield.attributedplaceholder = [[nsattributedstring alloc] initwithstring:@"placeholder" attributes:@{nsforegroundcolorattributename: [uicolor darkgraycolor], nsfontattributename: [uifont systemfontofsize:13]}];

因此,可以为uitextfeild创建category,专门用于处理修改placeholder属性提供方法

?
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
27
28
29
30
31
32
33
34
#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 的通过富文本方法修改属性。

?
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
27
28
29
#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 文章

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

 

模态弹窗属性 uimodalpresentationstyle 在 ios 13 下默认被设置为 uimodalpresentationautomatic新特性,展示样式更为炫酷,同时可用下拉手势关闭模态弹窗。

若原有模态弹出 viewcontroller 时都已指定模态弹窗属性,则可以无视该改动。

若想在 ios 13 中继续保持原有默认模态弹窗效果。可以通过 runtime 的 method swizzling 方法交换来实现。

?
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#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 状态。

?
1
2
3
if(@available(ios 13.0,*)){
self.window.overrideuserinterfacestyle = uiuserinterfacestylelight;
}

3.2 指定页面关闭黑暗模式

从xcode 11、ios 13开始,uiviewcontroller与view新增属性 overrideuserinterfacestyle,若设置view对象该属性为指定模式,则强制该对象以及子对象以指定模式展示,不会跟随系统模式改变。

  • 设置 viewcontroller 该属性, 将会影响视图控制器的视图以及子视图控制器都采用该模式
  • 设置 view 该属性, 将会影响视图及其所有子视图采用该模式
  • 设置 window 该属性, 将会影响窗口中的所有内容都采用该样式,包括根视图控制器和在该窗口中显示内容的所有控制器

3.3 全局适配黑暗模式

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

图片资源适配

打开图片资源管理库 assets.xcassets,选中需要适配的图片素材item,打开最右侧的 inspectors 工具栏,找到 appearances 选项,并设置为 any, dark模式,此时会在item下增加dark appearance,将黑暗模式下的素材拖入即可。关于黑暗模式图片资源的加载,与正常加载图片方法一致。

iOS13原生端适配攻略(推荐)

颜色适配

ios 13开始uicolor变为动态颜色,在light mode与dark mode可以分别设置不同颜色。若uicolor色值管理,与图片资源一样存储于 assets.xcassets 中,同样参照上述方法适配。若uicolor色值并没有存储于 assets.xcassets 情况下,自定义动态uicolor时,在ios 13下初始化方法增加了两个方法

?
1
2
+ (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回调

示例代码:

?
1
2
3
4
5
6
7
8
9
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动态颜色,具体声明如下:

?
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
@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 以下函数

?
1
2
3
4
5
// 注意:参数为变化前的traitcollection,改函数需要重写
- (void)traitcollectiondidchange:(uitraitcollection *)previoustraitcollection;
 
// 判断两个uitraitcollection对象是否不同
- (bool)hasdifferentcolorappearancecomparedtotraitcollection:(uitraitcollection *)traitcollection;

示例代码:

?
1
2
3
4
5
6
7
- (void)traitcollectiondidchange:(uitraitcollection *)previoustraitcollection {
 [super traitcollectiondidchange:previoustraitcollection];
 // trait has changed?
 if ([self.traitcollection hasdifferentcolorappearancecomparedtotraitcollection:previoustraitcollection]) {
 // do something...
 }
 }

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

当系统模式变更时,系统会通知所有的 view以及 viewcontroller 需要更新样式,会触发以下方法执行(参考apple官方适配链接):

nsview

?
1
2
3
4
- (void)updatelayer;
- (void)drawrect:(nsrect)dirtyrect;
- (void)layout;
- (void)updateconstraints;

uiview

?
1
2
3
4
5
- (void)traitcollectiondidchange:(uitraitcollection *)previoustraitcollection;
- (void)layoutsubviews;
- (void)drawrect:(nsrect)dirtyrect;
- (void)updateconstraints;
- (void)tintcolordidchange;

uiviewcontroller

?
1
2
3
4
- (void)traitcollectiondidchange:(uitraitcollection *)previoustraitcollection;
- (void)updateviewconstraints;
- (void)viewwilllayoutsubviews;
- (void)viewdidlayoutsubviews;

uipresentationcontroller

?
1
2
3
- (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 如下图,完成

iOS13原生端适配攻略(推荐)

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

 

在ios13之前,无需权限提示窗即可直接使用蓝牙,但在ios 13下,新增了使用蓝牙的权限申请。最近一段时间上传ipa包至app store会收到以下提示。

iOS13原生端适配攻略(推荐)

解决方案:只需要在 info.plist 里增加以下条目:

?
1
2
<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之前获取结果:

iOS13原生端适配攻略(推荐)

ios 13之后获取结果:

iOS13原生端适配攻略(推荐)

适配方案:目的是要将系统返回 nsdata 类型数据转换成字符串,再传给推送服务方。-(void)description; 本身是用于为类调试提供相关的打印信息,严格来说,不应直接从该方法获取数据并应用于正式环境中。将 nsdata 转换成 hexstring,即可满足适配需求。

?
1
2
3
4
5
6
7
8
9
- (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: 方法

?
1
2
3
4
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 比例

?
1
2
3
4
5
6
7
8
9
10
11
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之前的做适配即可。

?
1
2
3
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 官方链接

9. statusbar新增样式

 

statusbar 新增一种样式,默认的 default 由之前的黑色字体,变为根据系统模式自动选择展示 lightcontent 或者 darkcontent

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

原文链接:https://juejin.im/post/5da033756fb9a04e37316872